package nickthissen.iracing.parsing;

import nickthissen.iracing.common.*;
import nickthissen.iracing.common.Error;
import nickthissen.iracing.datamodels.PageItemList;
import nickthissen.iracing.datamodels.Post;
import nickthissen.iracing.datamodels.Result;
import nickthissen.iracing.datamodels.ThreadList;
import nickthissen.iracing.iracingforum2.R;

import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.util.Log;

import org.apache.commons.lang3.exception.ExceptionUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.nodes.Node;
import org.jsoup.nodes.TextNode;
import org.jsoup.parser.Tag;
import org.jsoup.select.Elements;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;

public abstract class Parser 
{
    private static final String LEFT_ARROW = "◄";
    private static final String RIGHT_ARROW = "►"; // "&#9658;";

    private LinkedList<String> logLines;
    private static final int MAX_LOGLINES = 10;

    protected abstract Result doParse(String source);
	protected abstract String getLogName();
    protected abstract Result createDefaultResultObject();

    public Parser()
    {
        logLines = new LinkedList<String>();
    }

	public Result parse(String source)
	{
        log("Start parsing.");

        Result result;
        try
        {
            // Try parsing the page
            result = this.doParse(source);
        }
        catch (Exception ex)
        {
            // If something went wrong, log it
            result = this.createDefaultResultObject();

            result.Success = false;

            StringBuilder builder = new StringBuilder();
            builder.append("Log:\n");
            for (String s : logLines)
            {
                builder.append(s + "\n");
            }
            builder.append("\nStacktrace:\n");
            builder.append(ExceptionUtils.getStackTrace(ex));

            result.DetailMessage = builder.toString();
            result.Exception = ex;
        }

		log("Finished parsing. Success = " + (result.Success ? "yes" : "no"));
		return result;
	}
	
	protected void log(String message)
	{
		Log.i(getLogName(), message);

        if (logLines.size() >= MAX_LOGLINES){
            logLines.removeFirst();
        }
        logLines.add(message);
	}

    protected boolean logLarge(Context context, String message)
    {
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
        boolean enableLogging = prefs.getBoolean(context.getString(R.string.PREF_DetailedLogging), false);
        if (!enableLogging) return false;

        if (message == null)
        {
            nickthissen.iracing.common.Error e = new Error();
            e.message = this.getLogName() + " source log.";
            e.detailMessage = "Source was null.";
            ServiceLogging.logError(context, e);
        }
        else
        {
            // Split source
            int length = message.length();
            int chunkSize = 10000;
            for (int i = 0; i < length; i += chunkSize) {
                String sub = message.substring(i, Math.min(length, i + chunkSize));

                nickthissen.iracing.common.Error e = new Error();
                e.message = this.getLogName() + " source log part " + i;
                e.detailMessage = sub;
                ServiceLogging.logError(context, e);
            }
        }
        return true;
    }

    protected void parsePagination(PageItemList itemlist, Document doc)
    {
        log("Start parsing pagination.");

        List<Element> pageNodes = new ArrayList<Element>();
        Elements divs = doc.select("div[class]");
        for (Element div : divs)
        {
            if (div.attr("class").equals("pagination"))
            {
                pageNodes.add(div);
                break;
            }
        }

        Element pageNode = null;
        if (pageNodes != null && pageNodes.size() > 0)
        {
            pageNode = pageNodes.get(0);
        }

        if (pageNode != null && pageNode.children().size() > 0)
        {
            itemlist.HasPagination = true;
        }
        else
        {
            itemlist.HasPagination = false;
            itemlist.ThreadLastPageUrl = itemlist.ThreadUrl;
            return;
        }

        // Find all TextNode or Element childnodes that have non-empty text:
        log("Finding pages.");
        List<Node> allChildren = pageNode.childNodes();
        List<Node> filtered = Linq.where(allChildren, new Predicate<Node>()
        {
            public boolean apply(Node in)
            {
                if (in instanceof Element) {
                    Element e = (Element)in;
                    return !TextUtils.isEmpty(e.text().trim());
                }
                if (in instanceof TextNode) {
                    TextNode t = (TextNode)in;
                    return !TextUtils.isEmpty(t.text().trim());
                }
                return false;
            }
        });


        Node[] childNodes = filtered.toArray(new Node[filtered.size()]);
        Node firstNode = childNodes[0];
        Node lastNode = childNodes[childNodes.length-1];

        log("Childnodes: " + childNodes.toString());

        // Find current page (first TextNode with numeric contents)
        TextNode currentPageNode = (TextNode)Linq.first(filtered, new Predicate<Node>()
        {
            public boolean apply(Node in)
            {
                if (in instanceof TextNode)
                {
                    // Check if numeric
                    String text = ((TextNode)in).text();
                    if (text == null) return false;
                    text = text.trim();
                    return TextUtils.isDigitsOnly(text);
                }
                return false;
            }
        });
        log("Current page node: " + currentPageNode.toString());
        itemlist.Page = Integer.parseInt(currentPageNode.text().trim());

        // Find last page
        Element lastPageNode = (Element)Linq.last(filtered, new Predicate<Node>()
        {
            public boolean apply(Node in)
            {
                if (in instanceof Element) {
                    return TextUtils.isDigitsOnly(((Element)in).text().trim());
                }
                return false;
            }
        });
        log("Last page node: " + lastPageNode.toString());
        itemlist.LastPage = Integer.parseInt(lastPageNode.text().trim());


        if (firstNode instanceof Element && ((Element)firstNode).text().trim().equals(LEFT_ARROW))
        {
            // Not the first page
            itemlist.PreviousPageUrl = firstNode.attr("href");
        }
        else
        {
            // First page
            itemlist.Page = 1;
            itemlist.PreviousPageUrl = "";
        }

        if (lastNode instanceof Element && ((Element)lastNode).text().trim().equals(RIGHT_ARROW))
        {
            // Not the last page
            itemlist.NextPageUrl = lastNode.attr("href");
            itemlist.ThreadLastPageUrl = childNodes[childNodes.length - 2].attr("href");
        }
        else
        {
            // Last page
            TextNode lastPageEl = (TextNode)lastNode;
            itemlist.Page = Integer.parseInt(lastPageEl.text().trim());
            itemlist.LastPage = itemlist.Page;
            itemlist.NextPageUrl = "";
            itemlist.ThreadLastPageUrl = itemlist.ThreadUrl;
        }

        log("Finished parsing pagination.");
    }

    protected void fixRelativeUrls(Document doc)
    {
        log("Start fixing relative URls.");
        Elements links = doc.body().select("a[href]");
        log("Links: " + links);

        for (Element e : links)
        {
            String href = e.attr("href");

            String absoluteUrl = Urls.getForumUrl(href);
            if (!e.className().equals("nolink") && absoluteUrl.startsWith("http://members.iracing.com/jforum/posts/downloadAttach/"))
            {
                // Handle attachment as special case
                e.attr("href", "link_" + absoluteUrl);
            }
            else
            {
                e.attr("href", absoluteUrl);
            }
        }

        Elements imgs = doc.body().select("img[src]");
        log("Images: " + imgs);

        for (Element e : imgs)
        {
            String src = e.attr("src");
            String absoluteUrl = Urls.getForumUrl(src);
            e.attr("src", absoluteUrl);
        }
    }

    protected void fixVideoTags(Document doc)
    {
        // Turn embedded video's into plain URLs
        Elements objectNodes = doc.select("object");

        for (int i = 0; i < objectNodes.size(); i++)
        {
            Element obj = objectNodes.get(i);
            Element parent = obj.parent();
            log("Fixing video tag: " + obj);

            Elements embedNodes = obj.select("embed");
            if (embedNodes.size() > 0)
            {
                Element embed = embedNodes.first();
                if (embed != null && embed.hasAttr("src"))
                {
                    String src = embed.attr("src");

                    // Replace video object with new URL element
                    Tag tag = Tag.valueOf("a");
                    Element link = new Element(tag, "");
                    link.attr("href", src);
                    link.text(src);

                    obj.replaceWith(link);
                }
            }
        }
    }

    protected void fixImageTags(Document doc)
    {
        Elements imgNodes = doc.select("img");
        for (int i = 0; i < imgNodes.size(); i++)
        {
            Element img = imgNodes.get(i);
            log("Fixing img tag: " + img);
            if (img.parent().nodeName().equals("a")) continue;

            String src = img.attr("src");
            img.wrap("<a href='" + src + "' class='nolink'></a>");
        }
    }

    protected long parseDateTimeScript(String script)
    {
        log("Datestring: " + script);
        int start = script.lastIndexOf("(") + 1;
        int end = script.indexOf(")");
        log("- start:" + start);
        log("- end:" + end);
        String msString = script.substring(start, end);
        log("MS string: " + msString);
        return Long.parseLong(msString);
    }
}
